﻿using System;
using System.IO.Ports;

namespace Js.Service
{
    #region Delegate

    /// <summary>
    /// 데이터가 송신되었을 때 발생합니다.
    /// </summary>
    /// <param name="sender">이벤트를 호출한 인스턴스입니다.</param>
    /// <param name="e">이벤트 정보입니다.</param>
    public delegate void SerialDataSendEventHandler(object sender, SerialDataSendEventArgs e);

    /// <summary>
    /// 데이터가 수신되었을 때 발생합니다.
    /// </summary>
    /// <param name="sender">이벤트를 호출한 인스턴스입니다.</param>
    /// <param name="e">이벤트 정보입니다.</param>
    public delegate void SerialDataRecvEventHandler(object sender, SerialDataRecvEventArgs e);

    #endregion Delegate

    /// <summary>
    /// 직렬 포트 통신을 하는 클래스입니다.
    /// </summary>
    public class SerialPortService : IDisposable
    {
        #region Variables

        private SerialPort _serialPort;

        #endregion Variables

        #region Properties

        /// <summary>
        /// Port 정보를 나타냅니다.
        /// </summary>
        public SerialPort Port
        {
            get { return _serialPort; }
        }

        /// <summary>
        /// 현재 수신버퍼 데이터가 있는지 여부를 나타냅니다.
        /// </summary>
        public bool CanReceive { get; private set; }

        #endregion Properties

        #region Event Handler

        /// <summary>
        /// 데이터가 송신되었을 때 발생합니다.
        /// </summary>
        public event SerialDataSendEventHandler DataSend;

        /// <summary>
        /// 데이터가 수신되었을 때 발생합니다.
        /// </summary>
        public event SerialDataRecvEventHandler DataRecv;

        #endregion Event Handler

        #region Constructor

        /// <summary>
        /// 지정된 포트 이름, 전송 속도, 패리티 비트, 데이터 비트 및 정지 비트를 사용하여
        /// Js.Service.SerialPortService 클래스의 새 인스턴스를 초기화합니다.
        /// </summary>
        /// <param name="portName">COM1과 같은 사용할 포트입니다.</param>
        /// <param name="baudRate">전송 속도입니다.</param>
        /// <param name="parity">System.IO.Ports.SerialPort.Parity 값 중 하나입니다.</param>
        /// <param name="dataBits">데이터 비트 값입니다.</param>
        /// <param name="stopBits">System.IO.Ports.SerialPort.StopBits 값 중 하나입니다.</param>
        /// <exception cref="System.IO.IOException">지정한 포트를 찾거나 열 수 없는 경우</exception>
        public SerialPortService(string portName = "COM1", int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One)
        {
            _serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
            _serialPort.ReadTimeout = SerialPort.InfiniteTimeout;
            _serialPort.WriteTimeout = SerialPort.InfiniteTimeout;
            _serialPort.DataReceived += new SerialDataReceivedEventHandler((sender, e) => CanReceive = true);
        }

        #endregion Constructor

        #region Static Method

        /// <summary>
        /// 현재 컴퓨터의 사용 가능한 직렬 포트 이름 배열을 가져옵니다.
        /// </summary>
        /// <returns>현재 컴퓨터의 사용 가능한 직렬 포트 이름 배열입니다.</returns>
        /// <exception cref="System.ComponentModel.Win32Exception">직렬 포트 이름을 쿼리하지 못한 경우 발생합니다.</exception>
        public static string[] GetPortNames()
        {
            return SerialPort.GetPortNames();
        }

        /// <summary>
        /// 직렬 통신에 사용되는 표준 전송속도 배열을 가져옵니다.
        /// </summary>
        /// <returns>직렬 통신에 사용되는 표준 전송속도 배열입니다.</returns>
        public static int[] GetBaudRates()
        {
            return new int[] { 2400, 4800, 9600, 19200, 38400, 57600, 115200, 312500 };
        }

        #endregion Static Method

        #region Method

        /// <summary>
        /// 지정된 Port를 통신이 가능하도록 합니다.
        /// </summary>
        public void Open()
        {
            if(!_serialPort.IsOpen)
            {
                _serialPort.Open();
            }
        }

        /// <summary>
        /// Js.Service.SerialPort 리소스를 해제합니다.
        /// </summary>
        public void Dispose()
        {
            if(_serialPort.IsOpen)
            {
                _serialPort.Close();
            }
        }

        /// <summary>
        /// 지정된 Port로 데이터를 송신합니다.
        /// </summary>
        /// <param name="tx">byte 배열 형식의 송신할 데이터입니다.</param>
        public void Send(byte[] tx)
        {
            _serialPort.BaseStream.Flush();
            _serialPort.Write(tx, 0, tx.Length);
            if(DataSend != null)
            {
                DataSend(this, new SerialDataSendEventArgs(DateTime.Now, tx));
            }
        }

        /// <summary>
        /// 지정된 Port로부터 지정된 시간 만큼 대기후 데이터를 수신합니다.
        /// </summary>
        /// <param name="responseTimeout">데이터 수신 응답 대기시간입니다.</param>
        /// <param name="transferTimeout">데이터가 전송이 완료될때 까지의 대기시간 입니다.</param>
        /// <returns>byte 배열 형식의 수신된 데이터입니다.</returns>
        public byte[] Receive(int responseTimeout, int transferTimeout)
        {
            int tick = Environment.TickCount;
            while(!CanReceive && !((Environment.TickCount - tick) >= responseTimeout)) ;
            if(CanReceive)
            {
                DelayService.Delay(transferTimeout);
                return Receive();
            }
            return null;
        }

        /// <summary>
        /// 지정된 Port로부터 데이터를 수신합니다.
        /// </summary>
        /// <returns>byte 배열 형식의 수신된 데이터입니다.</returns>
        public byte[] Receive()
        {
            int recvBufferSize = _serialPort.BytesToRead;
            byte[] rx = new byte[recvBufferSize];
            if(recvBufferSize > 0)
            {
                _serialPort.Read(rx, 0, recvBufferSize);
            }
            if(DataRecv != null)
            {
                DataRecv(this, new SerialDataRecvEventArgs(DateTime.Now, rx));
            }
            CanReceive = false;
            return rx.Length > 0 ? rx : null;
        }

        #endregion Method
    }
}